import utime
import machine
import ql_fs
from machine import UART, Pin
from .threading import Condition, Lock, Thread


class Serial(object):
    class TimeoutError(Exception):
        pass

    def __init__(self, port=2, baudrate=115200, bytesize=8, parity=0, stopbits=1, flowctl=0, rs485_config=None):
        self.__port = port
        self.__baudrate = baudrate
        self.__bytesize = bytesize
        self.__parity = parity
        self.__stopbits = stopbits
        self.__flowctl = flowctl
        self.__rs485_config = rs485_config

        self.__uart = None
        self.__r_cond = Condition()
        self.__w_lock = Lock()

    def __repr__(self):
        return '<UART{},{},{},{},{},{},{}>'.format(
            self.__port, self.__baudrate, self.__bytesize,
            self.__parity, self.__stopbits, self.__flowctl,
            self.__rs485_config
        )

    def __enter__(self):
        self.open()
        return self

    def __exit__(self, *args, **kwargs):
        self.close()

    @property
    def uart(self):
        if self.__uart is None:
            raise TypeError('uart not open.')
        return self.__uart

    def open(self):
        self.__uart = UART(
            getattr(UART, 'UART{}'.format(self.__port)),
            self.__baudrate,
            self.__bytesize,
            self.__parity,
            self.__stopbits,
            self.__flowctl
        )
        if isinstance(self.__rs485_config, dict):
            gpio_num = getattr(UART, "GPIO{}".format(self.__rs485_config['gpio_num']))
            direction = self.__rs485_config['direction']
            self.__uart.control_485(gpio_num, direction)

        self.__uart.set_callback(self.__uart_cb)

    def close(self):
        self.__uart.close()
        self.__uart = None

    def __uart_cb(self, _):
        with self.__r_cond:
            self.__r_cond.notify_all()

    def write(self, data):
        with self.__w_lock:
            return self.uart.write(data)

    def read(self, size, timeout=None):
        with self.__r_cond:
            if self.__r_cond.wait_for(lambda: self.uart.any() != 0, timeout=timeout):
                return self.uart.read(min(size, self.uart.any()))
            else:
                raise self.TimeoutError('serial read timeout.')


class Led(object):

    def __init__(self, GPIOn):
        self.__led = Pin(
            getattr(Pin, 'GPIO{}'.format(GPIOn)),
            Pin.OUT,
            Pin.PULL_PD,
            0
        )
        self.__off_period = 1000
        self.__on_period = 1000
        self.__count = 0
        self.__running_cond = Condition()
        self.__blink_thread = Thread(target=self.__blink_thread_worker)
        self.__blink_thread.start()

    @property
    def status(self):
        return self.__led.read()

    def on(self):
        with self.__running_cond:
            self.__count = 0
            return self.__led.write(1)

    def off(self):
        with self.__running_cond:
            self.__count = 0
            return self.__led.write(0)

    def blink(self, on_period, off_period, count):
        with self.__running_cond:
            self.__on_period = on_period
            self.__off_period = off_period
            self.__count = count
            self.__running_cond.notify_all()

    def __blink_thread_worker(self):
        while True:
            with self.__running_cond:
                self.__running_cond.wait_for(lambda: self.__count > 0)
                self.__led.write(1)
                utime.sleep_ms(self.__on_period)
                self.__led.write(0)
                utime.sleep_ms(self.__off_period)
                self.__count -= 1


class Buzzer(object):

    def __init__(self, GPIOn):
        self.__pin = Pin(
            getattr(Pin, 'GPIO{}'.format(GPIOn)),
            Pin.OUT,
            Pin.PULL_PD,
            0
        )

    def beep(self):
        self.__pin.write(1)

    def silent(self):
        self.__pin.write(0)


class I2CInterface(object):

    def __init__(self, i2c, slaveaddr):
        if not isinstance(i2c, machine.I2C):
            raise TypeError('`i2c` should be machine.I2C type')
        self.__i2c = i2c
        self.__slaveaddr = slaveaddr

    def read(self, addr, size=1, delay=0):
        """读寄存器

        :param addr: I2C 寄存器地址（bytes or bytearray）。
        :param size: 读取的字节长度。
        :param delay: 延时，数据转换缓冲时间(单位ms)，int类型。
        :return:
        """
        if size <= 0:
            raise ValueError('`size` should be greater than 0')
        data = bytearray(size)
        if self.__i2c.read(self.__slaveaddr, addr, len(addr), data, size, delay) == 0:
            return data
        else:
            return None

    def write(self, addr, data):
        """写寄存器

        :param addr: I2C 寄存器地址（bytes or bytearray）。
        :param data: 写入的数据，bytearray or bytes类型
        :return:
        """
        if not isinstance(data, (bytearray, bytes)):
            raise TypeError('`data` should be bytearray or bytes')
        return self.__i2c.write(self.__slaveaddr, addr, len(addr), data, len(data)) == 0


def deepcopy(obj):
    if isinstance(obj, (int, float, str, bool, type(None))):
        return obj
    if isinstance(obj, (list, tuple, set)):
        return type(obj)((deepcopy(item) for item in obj))
    elif isinstance(obj, dict):
        return {k: deepcopy(v) for k, v in obj.items()}
    else:
        raise TypeError('unsupported for \"{}\" type'.format(type(obj)))


class LocalStorage(object):

    def __init__(self):
        self.__path = None
        self.__db = {}
        self.__lock = Lock()

    def __repr__(self):
        return repr(self.__db)

    def from_object(self, obj):
        with self.__lock:
            self.__db.update({k: getattr(obj, k) for k in [k for k in dir(obj) if k.isupper()]})

    def from_json(self, path):
        with self.__lock:
            self.__path = path
            if not ql_fs.path_exists(path):
                raise ValueError('\"{}\" not exists!'.format(path))
            self.__db.update(ql_fs.read_json(path))

    def save(self, to_path=None):
        with self.__lock:
            to_path = to_path or self.__path
            if to_path is None:
                raise ValueError('no path to save.')
            ql_fs.touch(to_path, self.__db)

    def update(self, *args, **kwargs):
        with self.__lock:
            self.__db.update(*args, **kwargs)
        return self

    def get(self, key, default=None):
        with self.__lock:
            return deepcopy(self.__db.get(key, default))

    def __getitem__(self, key):
        with self.__lock:
            return deepcopy(self.__db[key])

    def __setitem__(self, key, value):
        with self.__lock:
            self.__db[key] = value
        return self
